import React, { useState, useEffect } from 'react' import { Navigate, useSearchParams, Link } from 'react-router-dom' import { Field, Form, Formik } from 'formik' import { trpc, client, inferMutationInput } from '~/utils/trpc' import IVInputField from '~/components/IVInputField' import IVButton from '~/components/IVButton' import { Organization } from '@prisma/client' import { notify } from '~/components/NotificationCenter' import { isOrgSlugValid } from '~/utils/validate' import useDashboard, { useHasPermission } from '~/components/DashboardContext' import Dialog, { useDialogState } from '~/components/IVDialog' import IVTextInput from '~/components/IVTextInput' import IVTooltip from '~/components/IVTooltip' import IVCheckbox from '~/components/IVCheckbox' import SlackIcon from '~/icons/compiled/Slack' import PageHeading from '~/components/PageHeading' import IVSpinner from '~/components/IVSpinner' import NavTabs, { NavTab } from '~/components/NavTabs' import classNames from 'classnames' function TabSubSection({ title, children, className, }: { title: string className?: string children: React.ReactNode }) { return (

{title}

{children}
) } export const ORG_SLUG_CONSTRAINTS = 'Must be at least 2 characters and can only contain lowercase letters, numbers, hyphens, and underscores.' export async function validateOrgSlugChange(id: string, slug: string) { if (!isOrgSlugValid(slug)) { return 'Slugs must be at least 2 characters and can only contain lowercase letters, numbers, hyphens, and underscores.' } const canChange = await client.query('organization.is-slug-available', { slug, id, }) if (!canChange) { return 'Sorry, this slug is taken. Please select another.' } } function EditOrganizationForm({ org }: { org: Organization }) { const ctx = trpc.useContext() const editOrganization = trpc.useMutation('organization.edit') return ( ['data']> initialValues={{ slug: org.slug, name: org.name, }} initialTouched={{ // We want any errors to display immediately on change, not on blur slug: true, }} onSubmit={async data => { if (editOrganization.isLoading) return editOrganization.mutate( { id: org.id, data, }, { onSuccess(newOrg) { if (newOrg.slug === org.slug) { notify.success('Your changes were saved.') ctx.refetchQueries(['organization.slug']) } else { window.location.assign( `/dashboard/${newOrg.slug}/organization/settings?nc-save-success` ) } }, } ) }} > {({ errors, touched }) => (
{ // skip server-side checks if value has not changed from default if (slug === org.slug) return return await validateOrgSlugChange(org.id, slug) }} />
{editOrganization.isError && (
Sorry, there was a problem updating the organization.
)}
)} ) } function OrganizationMFAForm() { const editMfa = trpc.useMutation('organization.edit.mfa') const session = trpc.useQuery(['auth.session.session']) const { organization, integrations } = useDashboard() const ctx = trpc.useContext() const canEnable = !!integrations?.workos && session.data?.hasMfa return ( > initialValues={{ requireMfa: organization.requireMfa, }} onSubmit={async data => { if (!canEnable || editMfa.isLoading) return editMfa.mutate(data, { onSuccess(newOrg) { notify.success('Your changes were saved.') ctx.setQueryData(['organization.slug', { slug: newOrg.slug }], { ...organization, requireMfa: newOrg.requireMfa, }) }, }) }} > {({ values, setFieldValue }) => (
{ setFieldValue('requireMfa', event.target.checked) }} helpText={ canEnable ? ( 'Any users without MFA enabled will immediately be prompted to enable it. This may interrupt workflows, so consider doing this at an off-peak time.' ) : ( <> Please{' '} enable MFA for your account {' '} before enabling this setting. ) } />
{editMfa.isError && (
Sorry, there was a problem updating the organization MFA settings.
)}
)} ) } function ArchiveForm() { const { organization, me } = useDashboard() const dialog = useDialogState() const archive = trpc.useMutation('organization.delete') const [slugConfirmation, setSlugConfirmation] = useState('') const { visible, animating } = dialog useEffect(() => { if (!visible && !animating) { setSlugConfirmation('') } }, [visible, animating]) const hasOtherOrganizations = me.userOrganizationAccess.filter( access => access.organization.ownerId === me.id ).length > 1 return (
{hasOtherOrganizations ? (
) } function SlackIntegrationsForm() { const startOauth = trpc.useMutation('organization.start-slack-oauth') const { organization } = useDashboard() const onClick = e => { e.preventDefault() if (startOauth.isLoading) return startOauth.mutate(null, { onSuccess(authUrl) { window.location.href = authUrl }, }) } const [searchParams, setSearchParams] = useSearchParams() const oauthMessage = code => { switch (code) { case 'success': return 'Connected your Slack workspace' case 'access_denied': return 'Must allow permissions to connect to Slack' case 'invalid_state_param': return 'Invalid OAuth state, try again' default: return 'Something went wrong' } } useEffect(() => { // check for some query param in redirect from // web/src/server/api/auth/oauth/slack.ts const oauthResult = searchParams.get('oauth_result') if (oauthResult) { const toastDuration = 4000 if (oauthResult === 'success' && organization.connectedToSlack) { notify.success(oauthMessage(oauthResult), { id: 'oauth-toast', duration: toastDuration, }) } else { // if 'success' but still not connected, override success message const result = oauthResult === 'success' ? 'Something went wrong' : oauthResult notify.error(oauthMessage(result), { id: 'oauth-toast', duration: toastDuration, }) } const errorTimeout = setTimeout(() => setSearchParams({}), toastDuration) return () => clearTimeout(errorTimeout) } }) return ( {organization.connectedToSlack ? ( <>

Your Slack workspace is connected to Interval 👍

Click here to reconnect .

To send notifications to a Slack channel , you'll also have to add the Interval app to that channel.

) : ( <>

Connect your Slack workspace to Interval to enable{' '} sending notifications to Slack channels or users .

{startOauth.error && (
{startOauth.error.message}
)} Add to Slack } onClick={onClick} /> )}
) } export default function OrganizationSettings() { const { organization, me, integrations } = useDashboard() const canConnectOauth = useHasPermission('WRITE_ORG_OAUTH') const canWriteSettings = useHasPermission('WRITE_ORG_SETTINGS') const [searchParams] = useSearchParams() const tab = searchParams.get('tab') if (canWriteSettings === undefined) { return } if (canWriteSettings === false) { return } let canAccessIntegrations = canConnectOauth && integrations?.slack const navItems: NavTab[] = [{ path: '', tab: null, label: 'General' }] if (integrations?.workos) { navItems.push({ path: '?tab=security', tab: 'security', label: 'Security', }) } if (canAccessIntegrations) { navItems.push({ path: '?tab=integrations', tab: 'integrations', label: 'Integrations', enabled: canConnectOauth, }) } return (
({ ...item, path: `/dashboard/${organization.slug}/organization/settings${item.path}`, }))} />
{tab === 'security' && integrations?.workos ? ( ) : tab === 'integrations' && canAccessIntegrations ? ( ) : ( <> {me.id === organization.ownerId && } )}
) }